package society.framework.core.communication;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

import society.framework.core.Automaton;
import society.framework.core.MessageList;
import society.framework.core.SocietyInformation;

/**
 * Automaton that manages the network communication between automatons in the local application and the remote application.
 * @author Micu Andrei
 *
 */
public class NetworkMessagingAutomaton extends Automaton {
	protected static final String CONNECTION_STATE = "CONNECTION_STATE";
	protected static final String IDLE_STATE = "IDLE_STATE";
	protected static final String SEND_MESSAGE_STATE = "SEND_MESSAGE_STATE";
	
	protected static final String MESSAGE_ENCODING = "UTF-8";
	
	protected MessageRouter messageRouter;
	
	protected Socket socket;
	protected DataInputStream inputStream;
	protected DataOutputStream outputStream;
	
	protected Thread pullingThread;
	protected boolean pullRunning;
	protected Thread pushingThread;
	protected boolean pushRunning;
	
	protected MessageList messagesToSend;
	
	protected enum Errors { PULL_ERROR, PUSH_ERROR };
	
	public NetworkMessagingAutomaton(MessageRouter messageRouter) {
		this.messageRouter = messageRouter;
		this.currentStateName = CONNECTION_STATE;
		this.messagesToSend = new MessageList();
		this.pullRunning = false;
		this.pushRunning = false;
		
		
		/* CONNECTION_STATE:  Automaton tries to connect to the server*/
		states.put(CONNECTION_STATE, new State() {
			
			@Override
			public void stateCode() {
				for(String address : SocietyInformation.getRemoteSocietyAddresses()){
					try {
						if(socket!=null){
							messagesToSend.cancelRemoving();
							socket.close();
						}
						MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
				                new Message("NetworkMessagingAutomaton",
				                		"Connecting to "+address+":"+ SocietyInformation.getRemoteMessagingPort() 
				                		+" for network messaging."));
						socket = new Socket();
						socket.connect(new InetSocketAddress(address, SocietyInformation.getRemoteMessagingPort()),
								SocietyInformation.RECONNECTION_TIMEOUT_FOR_TRANSFERS);
						if(socket.isConnected()){
							inputStream = new DataInputStream(socket.getInputStream());
							outputStream = new DataOutputStream(socket.getOutputStream());

							pullRunning = true;
							pushRunning = true;
							
							pullingThread = new Thread(){

								@Override
								public void run() {
									super.run();
									pullMessages();
								}
								
							};
							pushingThread = new Thread(){

								@Override
								public void run() {
									super.run();
									pushMessages();
								}
								
							};

							
							pullingThread.setName("NetworkMessagingPullingThread");
							pushingThread.setName("NetworkMessagingPushingThread");
							
							pullingThread.start();
							pushingThread.start();
							
							MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
					                new Message("NetworkMessagingAutomaton",
					                		"green:Connected successfully to "+address+":"+ SocietyInformation.getRemoteMessagingPort() 
					                		+" for network messaging."));
							break;
						} else {
							MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
					                new Message("NetworkMessagingAutomaton",
					                		"red:Failed to connect to "+address+":"+ SocietyInformation.getRemoteMessagingPort() 
					                		+" for network messaging."));
						}
					} catch (Exception e) {
						socket = null;
						MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
				                new Message("NetworkMessagingAutomaton",
				                		"red:Failed to connect to "+address+":"+ SocietyInformation.getRemoteMessagingPort() 
				                		+" for network messaging."));
					}
				}
			}
			
		});
		
		states.get(CONNECTION_STATE).transition = new Transition(){

			@Override
			public String transitionCode() {
				if(socket!=null){
					return IDLE_STATE;
				} else {
					return CONNECTION_STATE;
				}
			}
			
		};
		
		
		/* IDLE_STATE: Automaton waits for a message from the MessageRouter or from the threads. */
		
		states.put(IDLE_STATE, new NullState());
		
		states.get(IDLE_STATE).transition = new Transition(){

			@Override
			public String transitionCode() {
				getNextMessage();
				if(currentMessage.from.equals("NetworkMessagingAutomaton")){
					if(currentMessage.data.equals(Errors.PULL_ERROR) || currentMessage.data.equals(Errors.PUSH_ERROR)){
						return CONNECTION_STATE;
					} else {
						return IDLE_STATE;
					}
				} else {
					return SEND_MESSAGE_STATE;
				}
			}
			
		};
		
		
		/* SEND_MESSAGE_STATE: Automaton adds the message from the MessageRouter to the list of messages to send. */
		
		states.put(SEND_MESSAGE_STATE, new State(){

			@Override
			public void stateCode() {
				messagesToSend.add(currentMessage);
			}
			
		});
		
		states.get(SEND_MESSAGE_STATE).transition = new SimpleTransition(IDLE_STATE);
	}
	
	protected void pullMessages(){
		try {
			int lengthFrom, lengthTo, lengthData;
			byte[] buffer;
			String stringTo, stringFrom, stringData;
			while(pullRunning){				
				buffer = new byte[4];
				inputStream.read(buffer);
                lengthFrom = ((int) buffer[0] + 256) % 256;
                lengthFrom <<= 8;
                lengthFrom |= ((int) buffer[1] + 256) % 256;
                lengthFrom <<= 8;
                lengthFrom |= ((int) buffer[2] + 256) % 256;
                lengthFrom <<= 8;
                lengthFrom |= ((int) buffer[3] + 256) % 256;

                buffer = new byte[4];
				inputStream.read(buffer);
                lengthTo = ((int) buffer[0] + 256) % 256;
                lengthTo <<= 8;
                lengthTo |= ((int) buffer[1] + 256) % 256;
                lengthTo <<= 8;
                lengthTo |= ((int) buffer[2] + 256) % 256;
                lengthTo <<= 8;
                lengthTo |= ((int) buffer[3] + 256) % 256;

                buffer = new byte[4];
				inputStream.read(buffer);
                lengthData = ((int) buffer[0] + 256) % 256;
                lengthData <<= 8;
                lengthData |= ((int) buffer[1] + 256) % 256;
                lengthData <<= 8;
                lengthData |= ((int) buffer[2] + 256) % 256;
                lengthData <<= 8;
                lengthData |= ((int) buffer[3] + 256) % 256;
                
				
				buffer = new byte[lengthFrom];
				inputStream.readFully(buffer);
				stringFrom = new String(buffer, MESSAGE_ENCODING);

				buffer = new byte[lengthTo];
				inputStream.readFully(buffer);
				stringTo = new String(buffer, MESSAGE_ENCODING);
				
				buffer = new byte[lengthData];
				inputStream.readFully(buffer);
				stringData = new String(buffer, MESSAGE_ENCODING);
				
				messageRouter.send(stringTo, new Message(stringFrom, stringData));
			}
		} catch (Exception e) {
			NetworkMessagingAutomaton.this.addMessage(new Message("NetworkMessagingAutomaton", Errors.PULL_ERROR));
		}
	}
	
	protected void pushMessages(){
		try {
			Message message;
			byte[] from, to, data;
			byte[] fromLength = new byte[4], toLength = new byte[4], dataLength = new byte[4];
			while(pushRunning){
				message = messagesToSend.remove();
				if(message == null){
					return;
				}
				from = message.from.getBytes(MESSAGE_ENCODING);
				to = ((String[])message.data)[0].getBytes(MESSAGE_ENCODING);
				data = ((String[])message.data)[1].getBytes(MESSAGE_ENCODING);
				
				fromLength[0] = (byte)(from.length >> 24);
                fromLength[1] = (byte)(from.length >> 16);
                fromLength[2] = (byte)(from.length >> 8);
                fromLength[3] = (byte)from.length;

                toLength[0] = (byte)(to.length >> 24);
                toLength[1] = (byte)(to.length >> 16);
                toLength[2] = (byte)(to.length >> 8);
                toLength[3] = (byte)to.length;

                dataLength[0] = (byte)(data.length >> 24);
                dataLength[1] = (byte)(data.length >> 16);
                dataLength[2] = (byte)(data.length >> 8);
                dataLength[3] = (byte)data.length;
				
				outputStream.write(fromLength);
				outputStream.write(toLength);
				outputStream.write(dataLength);
				outputStream.write(from);
				outputStream.write(to);
				outputStream.write(data);
			}
		} catch (Exception e) {
			NetworkMessagingAutomaton.this.addMessage(new Message("NetworkMessagingAutomaton", Errors.PUSH_ERROR));
		}
	}
}
